"""
Workflow.py

* Copyright (c) 2006-2009, University of Colorado.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the University of Colorado nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF COLORADO ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF COLORADO BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

from collections import deque
import imp

import Components, pprint

class Workflow(object):

    def __init__(self, name):
        self.name        = name
        self.connections = {}


    def __str__(self):
        return '{name : %s, connections : %s}' % (self.name, self.connections)


    __repr__ = __str__


    def push_data_into_current_step(self, key, key_data):
        component, values = self.q[0]
        
        #if key not in values.keys():
        #    values[key] = []
        #values[key].append(key_data)
        values[key] = [key_data]
        
        self.q[0] = (component, values)

        
    def restore_workflow_state_to_step_num(self, step_num):
        self.step_num = int(step_num)
        self.q = self.history[self.step_num]


    def get_name(self):
        return self.name


    def load_component(self, name):
        # all components should come from Components
        # name = 'Components.' + name
        tup  = imp.find_module(name, Components.__path__)
        mod  = imp.load_module(name, *tup)
        comp = getattr(mod, name)
        instance = comp(self)
        return instance


    def instantiate(self):
       components = {}

       # load all components mentioned in workflow into memory
       # if component is a factor, retrieve the components
       # selected for this factor and load them as well
       names = self.connections.keys()
       for name in names:
           components[name] = self.load_component(name)

       for name in names:
           comp        = components[name]
           connections = self.connections[name]
           ports       = connections.keys()
           for port in ports:
               target  = connections[port]
               tcomp = components[target]
               comp.connect(tcomp, port)

       return components
        
        
    def init_workflow(self, workflow_name, initial_values):
        # {'Combiner': <Combiner.Combiner object at 0x7407f0>, 'Printer': <Printer.Printer object at 0x740850>}
        self.components = self.instantiate()
        
        # <Combiner.Combiner object at 0x7407f0>
        self.first_component = self.components[self.find_first_component()]
        
        self.step_num = 0
        self.history = {}
        
        # values = {'1': 1, '2': 2, '3': 3, '4': 4}
        # self.q = deque([([<Combiner.Combiner object at 0x7407f0>], {'1': 1, '3': 3, '2': 2, '4': 4})])
        # len(self.q) = 1
        self.q = deque([([self.first_component], initial_values)])
        
        
    def is_workflow_running(self, workflow_name):
        # returns True if workflow is running
        component, values = self.q[0]
        if component[0] != None: 
            return True
        else:        
            return False
        
        
    def is_last_component_done(self):
       # returns True if current component isn't connected ... aka "workflow finished running" 
        component, values = self.q[0]
        if component[0] == None: 
            return True
        else:        
            return False
       
                    
    def execute_one_step(self, workflow_name, initial_values):  
        # if the user didn't call init_workflow, make sure to call it before going on
        if not hasattr(self, 'first_component'):
            self.init_workflow(workflow_name, initial_values)

        # if self.q is empty, there's nothing to do
        if len(self.q) < 0:
            print('WARNING (in execute_one_step()): len(self.q) < 0')
            return

        # save state    (aka "provenance")
        import copy; 
        self.history[self.step_num] = copy.deepcopy(self.q)
        #print("STEP "+str(self.step_num)+":")
        #import pprint; pprint.pprint(self.history)
        
       
        # execute current step
        component, values = self.q.popleft()
        if component[0] != None:   
            return_value = (component[0])(values)
            next_component, next_values = return_value[0]            
            self.q.append((next_component, next_values))
            self.step_num += 1
          
        # Debug info  
        print("CURRENT STATE (step %d):" % self.step_num)
        pprint.pprint(self.q)
        



   
   
    def execute_until_input_is_needed(self, workflow_name, initial_values):  
        # if the user didn't call init_workflow, make sure to call it before going on
        if not hasattr(self, 'first_component'):
            self.init_workflow(workflow_name, initial_values)

        # keep stepping until user input is needed
        while True:

            # if self.q is empty, there's nothing to do
            if len(self.q) < 0:
                print('WARNING (in execute_one_step()): len(self.q) < 0')
                return

            # save state    (aka "provenance")
            import copy; 
            self.history[self.step_num] = copy.deepcopy(self.q)
            #print("STEP "+str(self.step_num)+":")
            #import pprint; pprint.pprint(self.history)
       
            # execute current step
            component, values = self.q.popleft()
            if component[0] != None:   
                # return_value[0] = 
                # ([<ReceiveGlass.ReceiveGlass object at 0x139bd10>],
                #  {'r': {'input_dict': {'manuf_date': {'type': 'date', 'value': '2111/11/30'}, 'next_step': 1, 'output_dict': {}, 'step_legend': ['1. Receive glass from manufacturers']}, 'status': 'awaiting input'})
                return_value = (component[0])(values)
                next_component, next_values = return_value[0]   
               
                self.q.append((next_component, next_values))
                self.step_num += 1
          
            # Debug info  
            print("CURRENT STATE (step %d):" % self.step_num)
            pprint.pprint(self.q)
            
            # break out condition
            if next_values['status'] == 'awaiting input':  	# possible vaues: 'running', 'awaiting input':
                break

   
   
   
   
   
   
        
    def get_values_in_current_step(self, workflow_name):
        component, values = self.q[0]
        
        # debug
#        print("IN: get_values_in_current_step()")
#        import pprint
#        pprint.pprint(values)   
        
        return values    # {'1': 1, 'status': 'awaiting input', '3': 3, '2': 2, '4': 4}
               
             
    def execute(self, workflow_name, init_values):
        # if the user didn't call init_workflow, make sure to call it before going on
        if not hasattr(self, 'first_component'):
            self.init_workflow(workflow_name, init_values)
        
        # bad ass pythonic execution algorithm written by Evan Sheehan
        # gist: 
        #       1. create a queue that contains a tuple of the first
        #          component of the workflow and the set of values
        #          to process
        #       2. pull the first tuple off the queue, get its component
        #          and pass the set of values to it. Note: components
        #          are 'callable' objects to enable this style of
        #          processing
        #       3. When a component is invoked, it processes the set of
        #          values handed to it and returns a tuple of the next
        #          component to execute and the values it needs to process
        #       4. This tuple gets added to the queue and we continue
        #          to invoke components and add tuples to the queue until
        #          the queue is empty.
        #       Note: we only add a tuple to the queue if it a) has greater
        #             than zero values to process and 2) is not already in
        #             the queue. This ensures that the queue eventually
        #             empties out.
                
        # values = {'1': 1, '2': 2, '3': 3, '4': 4}
        # self.q = deque([([<Combiner.Combiner object at 0x7407f0>], {'1': 1, '3': 3, '2': 2, '4': 4})])
        # len(self.q) = 1
        while len(self.q):
            import copy; self.history[self.step_num] = copy.deepcopy(self.q)
            print("STEP "+str(self.step_num)+":")
            import pprint; pprint.pprint(self.history)
            
            component, values = self.q.popleft()
            if component[0] == None:
                break
            
            new_step = input('Enter <step#><ENTER> or just <ENTER> to execute step '+str(self.step_num)+':')
            
            # a specific step number was entered
            if len(new_step)>0:
                self.step_num = int(new_step)
                self.q = self.history[self.step_num]
            
            # Enter was pressed
            else:
                print('ELSE')
                pprint.pprint(component)
                return_value = (component[0])(values)
                next_component, next_values = return_value[0]
                self.q.append((next_component, next_values))
                self.step_num += 1

          # SIMPLIFIED ORIGINAL EXECUTION ALGORITHM
              #component, values = self.q.popleft()
            #return_value = (component[0])(values)
            #next_component, next_values = return_value[0]
            #self.q.append((next_component, next_values))

            # ORIGINAL EXECUTION ALGORITHM
            # pending = (([<Increment.Increment object at 0x73e250>], {'1': 1, '2': 2, '3': 3, '4': 4, '5': 4, '6': 6}),)
            #for pending in map(lambda comp: comp(values), components):
                #for pair in pending:
                    #if len(pair[1]) and pending not in self.q:
                        #self.q.append(pair)


    def find_first_component(self):
        notFirst = []
        names = self.connections.keys()
        for name in names:
            mappings = self.connections[name]
            ports = mappings.keys()
            for port in ports:
                destination = mappings[port]
                if destination != None:
                    if not destination in notFirst:
                        notFirst.append(destination)
        first_comp = [name for name in names if not name in notFirst]
        if len(first_comp) == 1:
            return first_comp[0]
        raise Exception("Workflow does not have a clear first component")


    def load(self, file):
        self.connections.clear()
        f = open(file, "U")
        line = f.readline().strip()
        while line != "":
            index           = line.find("=>")
            source          = line[0:index-1]
            destination     = line[index+3:]

            port_index      = source.find(".")
            if port_index > 0:
                port   = source[port_index+1:]
                source = source[0:port_index]
            else:
                port   = 'output'

            if not source in self.connections:
                self.connections[source] = {}
            if not destination in self.connections:
                self.connections[destination] = {}

            mappings = self.connections[source]
            mappings[port] = destination

            line = f.readline().strip()
        f.close()
        
        
    def save(self, path):
        pass
